#include "ray.h"	// vector.h already included
//include "vector.h"
#include <cmath>
#include <cstdlib>

#define MIN(a,b) (a < b ? a : b)
#define MAX(a,b) (a > b ? a : b)
#define EPSILON 1.0E-6f

int GetFreeObject (Object *scene)
{
	for (int i=0; i < MAX_OBJECTS; i++)
	{
		if (scene[i].active == 0)
			return i;
	}
	return -1;
}

Object *AddSphere(Object *scene, Vector3 origin, float radius, Color color)
{
	int index = GetFreeObject(scene);
	if (index >= 0)
	{
		scene[index].active = 1;
		scene[index].type = t_sphere;
		scene[index].sphere.origin[0] = origin[0];
		scene[index].sphere.origin[1] = origin[1];
		scene[index].sphere.origin[2] = origin[2];
		scene[index].sphere.radius = radius;
		scene[index].color = color;
		scene[index].reflect = 0.8f; // default reflective
		scene[index].specular = 0.8f; // default specular

		return &scene[index];
	}

	return NULL;
}

Object *AddBox (Object *scene, Vector3 origin, float x, float y, float z, Color color)
{
	int index = GetFreeObject(scene);
	if (index >= 0)
	{
		scene[index].active = 1;
		scene[index].type = t_box;
		scene[index].box.origin[0] = origin[0];
		scene[index].box.origin[1] = origin[1];
		scene[index].box.origin[2] = origin[2];
		scene[index].box.x = x;
		scene[index].box.y = y;
		scene[index].box.z = z;
		scene[index].color = color;
		scene[index].reflect = 0.0f;
		scene[index].specular = 0.0f;

		return &scene[index];
	}

	return NULL;
}

Object *AddPlane (Object *scene, Vector3 normal, float dist, Color color)
{
	int index = GetFreeObject(scene);
	if (index >= 0)
	{
		scene[index].active = 1;
		scene[index].type = t_plane;
		VectorCopy(normal, scene[index].plane.normal);
		scene[index].plane.dist = dist;
		scene[index].color = color;
		scene[index].reflect = 0.0f;
		scene[index].specular = 0.0f;

		return &scene[index];
	}	

	return NULL;
}

Object *AddDisc(Object *scene, Vector3 normal, Vector3 origin, float radius, Color color)
{
	int index = GetFreeObject(scene);
	if (index >= 0)
	{
		scene[index].active = 1;
		scene[index].type = t_disc;
		VectorCopy(normal, scene[index].disc.normal);
		VectorCopy(origin, scene[index].disc.origin);
		scene[index].disc.radius = radius;
		scene[index].color = color;
		scene[index].reflect = 0.0f;
		scene[index].specular = 0.7f;

		return &scene[index];
	}	

	return NULL;
}

Object *AddLight(Object *scene, Vector3 origin, float radius, Color color)
{
	int index = GetFreeObject(scene);
	if (index >= 0)
	{
		scene[index].active = 1;
		scene[index].type = t_light;
		scene[index].light.origin[0] = scene[index].sphere.origin[0] = origin[0];
		scene[index].light.origin[1] = scene[index].sphere.origin[1] = origin[1];
		scene[index].light.origin[2] = scene[index].sphere.origin[2] = origin[2];
		scene[index].light.radius = radius;
		scene[index].sphere.radius = 6;
		scene[index].color = color;
		scene[index].reflect = 0.0f;
		scene[index].specular = 0.0f;

		return &scene[index];
	}

	return NULL;
}

float CheckCollisions (Object *scene, Vector3 start, Vector3 unitVector, int ignore, bool ignoreLights, float hitBreak, Vector3 normal, int *hitOut)
{
	int hit;
	float dist = HUGE-100.0f, getDist;
	Vector3 getNormal;

	// run through potential objects to hit
	hit = -1;
	for (int i=0; i < MAX_OBJECTS; i++)
	{
		if (!scene[i].active || (ignoreLights && scene[i].type == t_light))
			continue;
		if (i == ignore)
			continue;

		// trace against object &scene[i]
		// if trace is shorter than last one, update end vector and objectColor
		if (scene[i].type == t_sphere)
			getDist = IntersectSphereWS(scene[i].sphere, start, unitVector, getNormal);
		else if (scene[i].type == t_plane)
			getDist = IntersectPlaneWS(scene[i].plane, start, unitVector, getNormal);
		else if (scene[i].type == t_box)
			getDist = IntersectBboxWS(scene[i].box, start, unitVector, getNormal);
		else if (scene[i].type == t_disc)
			getDist = IntersectDiscWS(scene[i].disc, start, unitVector, getNormal);
		else if (scene[i].type == t_light)
		{
			// NOTE: Lights need their sphere field filled in too
			getDist = IntersectSphereWS(scene[i].sphere, start, unitVector, getNormal);
		}
		else
			continue;

		if (getDist < dist && getDist > 0)
		{
			dist = getDist;
			if (normal != NULL)
			{
				normal[0] = getNormal[0];
				normal[1] = getNormal[1];
				normal[2] = getNormal[2];
			}
			hit = i;

			// hitBreak distance is used for shadowing
			// once something is found to be in the way, don't bother continuing
			if (hitBreak > 0 && dist < hitBreak)
				break;
		}
	}

	if (hitOut != NULL)
		*hitOut = hit;
	return dist;
}

Color CastRay (Object *scene, Vector3 start, Vector3 unitVector, int bounces)
{
	Color result = {1.0f, 1.0f, 0.0f};
	Color diffuse = {0.0f, 0.0f, 0.0f}, specular = {0.0f, 0.0f, 0.0f}, reflection = {0.0f, 0.0f, 0.0f};
	int i, hit;
	Vector3 end, normal;
	Vector3 newUnitVector;
	float dist = HUGE-100.0f, checkDist, getDist;

	// find the object hit by the ray
	dist = CheckCollisions(scene, start, unitVector, -1, false, -1.0f, normal, &hit);	

	// if we have a hit, check lighting conditions
	if (hit >= 0)
	{
		result.dist = dist;
		if (scene[hit].type == t_light) // ignore lighting on lights, just show fullbright shape
			result = scene[hit].color;
		else
		{
			result.r = result.g = result.b = 0.0f;
	
			// get ending point based on distance and unit vector
			end[0] = start[0] + unitVector[0]*dist;
			end[1] = start[1] + unitVector[1]*dist;
			end[2] = start[2] + unitVector[2]*dist;			

			// get lighting contribution for each light
			for (i=0; i < MAX_OBJECTS; i++)
			{
				bool shadow = false;
				if (!scene[i].active || scene[i].type != t_light)
					continue;

				VectorSubtract(scene[i].light.origin, end, newUnitVector);
				checkDist = VectorSize(newUnitVector); // total size before normalizing
				Normalize(newUnitVector);

				getDist = CheckCollisions (scene, end, newUnitVector, hit, true, checkDist, NULL, NULL);
				if (getDist < checkDist)
				{
					shadow = true;
					//printf("Shadow!\n");
				}
	
				// if the light is not behind another object, add its contribution to the total color
				if (!shadow)
				{
					// calculations based on light color, object color, attenuation and dot product
					float atten = MAX(0.0f, 1.0f - (checkDist / scene[i].light.radius));

					// add diffuse
					diffuse.r += scene[i].color.r * scene[hit].color.r * atten * MAX(0.0f, DotProduct(normal, newUnitVector));
					diffuse.g += scene[i].color.g * scene[hit].color.g * atten * MAX(0.0f, DotProduct(normal, newUnitVector));
					diffuse.b += scene[i].color.b * scene[hit].color.b * atten * MAX(0.0f, DotProduct(normal, newUnitVector));

					// add specular highlights
					Vector3 reflVec;
					for (int v=0; v < 3; v++)
						reflVec[v] = 2*normal[v] + unitVector[v];
					Normalize(reflVec);
					float spec = MAX(0.0f, DotProduct(reflVec, newUnitVector));
					spec = pow(spec, 120.0f);
					specular.r += scene[i].color.r * spec;
					specular.g += scene[i].color.g * spec;
					specular.b += scene[i].color.b * spec;

					// clamp results
					diffuse.r = MIN(1.0f, diffuse.r);
					diffuse.g = MIN(1.0f, diffuse.g);
					diffuse.b = MIN(1.0f, diffuse.b);

					specular.r = MIN(1.0f, specular.r);
					specular.g = MIN(1.0f, specular.g);
					specular.b = MIN(1.0f, specular.b);
				}
			}

			// add contributions from reflection where appropriate
			if (bounces > 0 && scene[hit].reflect > 0.0001f)
			{
				Color reflColor;
				Vector3 reflVec, reflStart;
				for (int v=0; v < 3; v++)
					reflVec[v] = 2*normal[v] + unitVector[v];
				Normalize(reflVec);
				VectorAdd(end, reflVec, reflStart);
				reflColor = CastRay(scene, reflStart, reflVec, bounces-1);
				reflection.r = MIN(1.0f, reflection.r + reflColor.r);
				reflection.g = MIN(1.0f, reflection.g + reflColor.g);
				reflection.b = MIN(1.0f, reflection.b + reflColor.b);
			}

			// add separate components of calculations to make final result
			float refl = scene[hit].reflect;
			result.r = MIN(1.0f, diffuse.r*(1.0f-refl) + reflection.r*refl + specular.r*scene[hit].specular);
			result.g = MIN(1.0f, diffuse.g*(1.0f-refl) + reflection.g*refl + specular.g*scene[hit].specular);
			result.b = MIN(1.0f, diffuse.b*(1.0f-refl) + reflection.b*refl + specular.b*scene[hit].specular);
		}
	}
	else
	{
		result.r = 0.0f;
		result.g = 0.0f;
		result.b = 0.5f; // clear color
		result.dist = HUGE;
	}

	return result;
}

// IntersectSphereWS takes world space coordinates and translates them to IntersectSphere function (object space)
float IntersectSphereWS (Sphere sphere, Vector3 start, Vector3 unitVector, Vector3 normal)
{
	Vector3 startOS;
	startOS[0] = start[0] - sphere.origin[0];
	startOS[1] = start[1] - sphere.origin[1];
	startOS[2] = start[2] - sphere.origin[2];

	return IntersectSphere(sphere, startOS, unitVector, normal);
}

// IntersectBboxWS takes world space coordinates and translates them to IntersectBbox function
float IntersectBboxWS(Box box, Vector3 start, Vector3 unitVector, Vector3 normal)
{
	Vector3 startOS;
	startOS[0] = start[0] - box.origin[0];
	startOS[1] = start[1] - box.origin[1];
	startOS[2] = start[2] - box.origin[2];

	return IntersectBbox(box, startOS, unitVector, normal);
}


// Written by Elliot Drown, based on commonly available formulae
float IntersectPlaneWS (Plane plane, Vector3 start, Vector3 unitVector, Vector3 normal)
{
	float t;
	Vector3 diff;

	if (fabsf(DotProduct(unitVector, plane.normal)) < EPSILON)	// close to 0 => orthogonal normal
		return HUGE;											// => plane is parallel to ray

	for (int i=0; i < 3; i++)
		diff[i] = start[i] - (plane.normal[i]*plane.dist); // start - point on plane
	t = -DotProduct(plane.normal, diff)/DotProduct(plane.normal, unitVector);

	if (t < 0)
		return HUGE;

	VectorCopy(plane.normal, normal);
	Normalize(normal);

	return t;
}

float IntersectDiscWS (Disc disc, Vector3 start, Vector3 unitVector, Vector3 normal)
{
	Plane plane;
	Vector3 diff, point;
	float t;

	// line/plane intersection to get plane distance
	t = DotProduct(disc.normal, unitVector);
	if (fabsf(t) < EPSILON)	// close to 0 => orthogonal normal => disc is parallel to ray
		return HUGE;			

	plane.dist = DotProduct(disc.normal, disc.origin);
	VectorCopy(disc.normal, plane.normal);
	t = IntersectPlaneWS(plane, start, unitVector, normal);
	if (t >= HUGE) // if no collision at first, try backwards facing plane
	{
		for (int x=0; x < 3; x++)
			plane.normal[x] = -disc.normal[x];
		Normalize(plane.normal);
		plane.dist = DotProduct(plane.normal, disc.origin);
		t = IntersectPlaneWS(plane, start, unitVector, normal);
	}
	if (t < HUGE)
	{
		// scale the unit vector to get intersection point
		for (int i = 0; i < 3; i++)
			point[i] = (t * unitVector[i]) + start[i];

		VectorSubtract(disc.origin, point, diff);	// distance
		if (VectorSize(diff) > disc.radius)
			t = HUGE;		// point not inside circle		
	}

	return t;
}

// ************************
// NOTICE: Cited code below
// ************************

// IntersectSphere
// Code from MIT: http://groups.csail.mit.edu/graphics/classes/6.837/F99/assignment6/ivray/RayCast.C
// Adapted by our team for use with our code.
// (Takes object space vectors)
float IntersectSphere (Sphere sphere, Vector3 start, Vector3 unitVector, Vector3 normal)
{
	float b = 2.0f*(start[0]*unitVector[0] + start[1]*unitVector[1] + start[2]*unitVector[2]);
	float c = start[0]*start[0] + start[1]*start[1] + start[2]*start[2] - sphere.radius*sphere.radius;
	float d = b*b - 4*c;
	float t;

	if (d < 0.0)		// no intersection
		return HUGE;
	else if (d == 0.0)  	// line tangent to sphere
	{
		t = -b / (2.0);
		if (t < 0)  return HUGE;
	}
	else 			// line intersects sphere
	{
		float t1 = (-b + sqrt(d)) / (2.0);
		float t2 = (-b - sqrt(d)) / (2.0);

		if (t2 <= 0)
		{
			if (t1 <= 0)
				return HUGE;
			else
				t = t1;
		}
		else  t = t2;
	}

	// Construct surface normal:
	normal[0] = start[0] + t * unitVector[0];
	normal[1] = start[1] + t * unitVector[1];
	normal[2] = start[2] + t * unitVector[2];
	// normalize the normal vector
	Normalize(normal);

	return t;	
}

// IntersectBBox
// Code from MIT: http://groups.csail.mit.edu/graphics/classes/6.837/F99/assignment6/ivray/RayCast.C
// Adapted by our team for use with our code.
// (Takes object space vectors)
float IntersectBbox(Box box, Vector3 rayorigOS, Vector3 raydirOS, Vector3 surfnOS)
{
	// parallelepiped is centered at origin
	// get half-width, height, depth of box
	Vector3 min = {-box.x/2, -box.y/2, -box.z/2};
	Vector3 max = {box.x/2, box.y/2, box.z/2};

	// ray is of form rayorigOS + t * raydirOS
	// find smallest t such that ray intersects box.
	float tmin = -HUGE, tmax = HUGE;
	float t1, t2;

	Vector3 axes[3] = {{1, 0, 0}, {0, 1, 0}, {0, 0, 1}};

	char maxNormSet = 0, minNormSet = 0;
	Vector3 maxNorm, minNorm;

	// box must have non-zero volume
	if (max[0] <= min[0] || max[1] <= min[1] || max[2] <= min[2])
		return HUGE;

	// intersect ray with x,y,z slabs
	for (int k = 0; k < 3; k++)
	{
		// check ray against planes xk = -dx_k, xk = +dx_k
		if (raydirOS[k] != 0.0)
		{
			t1 = (min[k] - rayorigOS[k]) / raydirOS[k];
			t2 = (max[k] - rayorigOS[k]) / raydirOS[k];

			tmin = fmax ( tmin, fmin (t1, t2) );
			tmax = fmin ( tmax, fmax (t1, t2) );

			if (t1 > 0 && tmin == t1)         // neg face
			{
				VectorSet(minNorm, -axes[k][0], -axes[k][1], -axes[k][2]);
				minNormSet = 1;
			}
          		else if (t2 > 0 && tmin == t2)    // pos face
            		{
				VectorSet(minNorm, axes[k][0], axes[k][1], axes[k][2]);
              			minNormSet = 1;
            		}
          		else if (t1 > 0 && tmax == t1)    // neg face
            		{
				VectorSet(maxNorm, -axes[k][0], -axes[k][1], -axes[k][2]);
              			maxNormSet = 1;
            		}
          		else if (t2 > 0 && tmax == t2)    // pos face
            		{
				VectorSet(maxNorm, axes[k][0], axes[k][1], axes[k][2]);
              			maxNormSet = 1;
            		}
        	}
		else
		{
	  		if ((rayorigOS[k] < min[k]) || (rayorigOS[k] > max[k]))
	    			return HUGE;
		}
	} // k loop

	// tmin..tmax now define intersection with parallelepiped
	if (tmin >= tmax) // intersection is empty or a point
		return HUGE;
	else if (tmin > 0)
	{
		if (minNormSet)
			VectorCopy(minNorm, surfnOS);
		return tmin;
	}
	else if (tmax > 0)
	{
		if (minNormSet)
			VectorCopy(maxNorm, surfnOS);
		return tmax;
	}
	else   // no hit
		return HUGE;
}
